1 /** 2 This module provides a function for pretty-printing D arrays of various dimensions. 3 A multidimensional array is represented as a 2D matrix surrounded by nested square frames. 4 If the array is too big, it will be truncated accordingly. 5 The surrounding frame, and the truncation symbol can be changed as well as truncation options. 6 */ 7 module pretty_array; 8 9 import std.array : join, array; 10 import std.conv : to; 11 import std.utf : byCodeUnit; 12 import std.typecons : tuple, Tuple; 13 import mir.ndslice; 14 15 /// TODO: a placeholder string for NaNs 16 enum NAN = "nan"; 17 /// TODO: a placeholder string for Infs 18 enum INF = "inf"; 19 20 /// pretty_array formatting configuration. 21 private enum Format : int 22 { 23 edgeitems = 3, // sets N leading and trailing items for each dimension 24 threshold = 300, // max N array elements allowed without truncation 25 precision = 8, // TODO: precision of floating point representations 26 suppressExp = 0, // TODO: suppress printing small floating values in exp format 27 lineWidth = 120 28 } 29 30 private enum Frame : string 31 { 32 ltAngle = "┌", 33 lbAngle = "└", 34 rtAngle = "┐", 35 rbAngle = "┘", 36 vBar = "│", 37 newline = "\n", 38 dash = "─", 39 dot = "·", 40 space = " ", 41 truncStr = "░" // TIP: length of this string is 3! 42 } 43 44 ulong[] getShape(T : int)(T obj, ulong[] dims = null) 45 { 46 return dims; 47 } 48 49 ulong[] getShape(T : double)(T obj, ulong[] dims = null) 50 { 51 return dims; 52 } 53 54 /++ 55 Get the shape of a plain D array. 56 A standalone convenience function for getting array shape without converting to Mir Slices. 57 The array must have correct dimensions otherwise the column index will not be consistent. 58 +/ 59 ulong[] getShape(T)(T obj, ulong[] dims = null) 60 in 61 { 62 import std.traits : isArray; 63 64 assert(isArray!(typeof(obj))); 65 } 66 do 67 { 68 dims ~= obj.length.to!int; 69 return getShape!(typeof(obj[0]))(obj[0], dims); 70 } 71 72 // Calculate the length of array elements converted to strings. 73 private ulong getStrLength(T)(T arrSlice) 74 { 75 if (arrSlice.shape.length == 1) 76 { 77 return arrSlice.map!(a => a.to!string).join.length; 78 } 79 else 80 { 81 auto slice2D = arrSlice.flattened.chunks(arrSlice.shape[$ - 1]); 82 return slice2D[0].map!(a => a.to!string).join.length; 83 } 84 } 85 86 // Convert truncated index to real array index. 87 private ulong convertTruncIdx(ulong idx, ulong truncLen, ulong rowLen) 88 { 89 pragma(inline, true); 90 return idx > Format.edgeitems ? rowLen - (truncLen - idx) : idx; 91 } 92 93 /++ 94 Get the longest string length of a row, construct a row with the longest string elements. 95 We need to know the longest string length of the row to calculate the correct padding between the frames. 96 We need to keep the row with longest string elements to correctly right-align all array elements. 97 +/ 98 private Tuple!(ulong, "strlen", string[], "row") getMaxStrLenAndMaxRow(T)(T arrSlice, bool truncate) 99 { 100 101 auto slice2D = arrSlice.flattened.chunks(arrSlice.shape[$ - 1]); 102 const ulong truncLen = Format.edgeitems * 2 + 1; 103 const bool enoughRows = slice2D.shape[0] > truncLen; 104 const bool encoughCols = slice2D[0].length > truncLen; 105 ulong maxStrRowLen; 106 string[] row, maxRow; 107 108 // fill the empty array with the number of row elements 109 // there probably a better way to do it 110 for (int i; i < (truncate && encoughCols ? truncLen : slice2D[0].length); i++) 111 { 112 maxRow ~= "0"; 113 row ~= ""; 114 } 115 116 // construct a row with longest string elements 117 ulong rowi, colj; 118 for (ulong i; i < (truncate && enoughRows ? truncLen : slice2D.shape[0]); i++) 119 { 120 rowi = truncate && enoughRows ? convertTruncIdx(i, truncLen, slice2D.shape[0]) : i; 121 for (ulong j; j < (truncate && encoughCols ? truncLen : slice2D[rowi].length); 122 j++) 123 { 124 colj = truncate && encoughCols ? convertTruncIdx(j, truncLen, slice2D[i].length) : j; 125 row[j] = slice2D[rowi][colj].to!string; 126 } 127 128 for (ulong k; k < row.length; k++) 129 { 130 if (truncate && encoughCols && (k == Format.edgeitems)) 131 { 132 maxRow[k] = Frame.truncStr; 133 continue; 134 } 135 maxRow[k] = maxRow[k].length < row[k].length ? row[k] : maxRow[k]; 136 } 137 } 138 maxStrRowLen = truncate && encoughCols 139 ? maxRow.join.length + truncLen - 3 : maxRow.join.length + slice2D[0].length - 1; // -3 because Frame.truncStr.length == 3 140 return Tuple!(ulong, "strlen", string[], "row")(maxStrRowLen, maxRow); 141 } 142 143 /++ 144 Construct the padding between frame angles. 145 Use white space if padding string is not provided. 146 +/ 147 private string getPadding(T)(T arrShape, ulong maxStrRowLen, string padStr = Frame.space) 148 { 149 return padStr.byCodeUnit.repeat((arrShape.length < 2 150 ? 0 : arrShape.length - 2) * 2 + maxStrRowLen).join; 151 } 152 153 private ulong lenDiff()(string a, string b) 154 { 155 return a.length > b.length ? a.length - b.length : 0; 156 } 157 158 private string prettyFrame(T)(T arrSlice, bool truncate) 159 if (arrSlice.shape.length == 1) 160 { 161 if (truncate) 162 { 163 string[] leftSlice = arrSlice[0 .. Format.edgeitems].map!(a => a.to!string).array; 164 string[] rightSlice = arrSlice[$ - Format.edgeitems .. $].map!(a => a.to!string).array; 165 return Frame.vBar ~ (leftSlice ~ Frame.truncStr ~ rightSlice) 166 .join(" ") ~ Frame.vBar ~ Frame.newline; 167 } 168 else 169 { 170 171 return Frame.vBar ~ arrSlice.map!(a => a.to!string).join(" ") ~ Frame.vBar ~ Frame.newline; 172 } 173 174 } 175 176 private string prettyFrame(T)(T arrSlice, string addedFrame, Tuple!(ulong, 177 "strlen", string[], "row") maxRow, bool truncate) 178 if (arrSlice.shape.length == 2) 179 { 180 string arrStr; 181 ulong rowi, colj; 182 const ulong truncLen = Format.edgeitems * 2 + 1; 183 const bool enoughRows = arrSlice.shape[0] > truncLen; 184 const bool enoughCols = arrSlice.shape[1] > truncLen; 185 186 for (ulong i; i < (truncate && enoughRows ? truncLen : arrSlice.shape[0]); i++) 187 { 188 string[] newRow; 189 rowi = truncate && enoughRows ? convertTruncIdx(i, truncLen, arrSlice.length) : i; 190 for (ulong j; j < (truncate && enoughCols ? truncLen : arrSlice[rowi].length); 191 j++) 192 { 193 colj = truncate && enoughCols ? convertTruncIdx(j, truncLen, arrSlice[i].length) : j; 194 // insert white spaces before the element to right align it 195 newRow ~= " ".repeat(lenDiff(maxRow.row[j], 196 arrSlice[rowi][colj].to!string)).join ~ arrSlice[rowi][colj].to!string; 197 198 if (truncate && enoughCols && (j == Format.edgeitems)) 199 { 200 newRow[$ - 1] = Frame.truncStr; // overwrite last with truncation string 201 } 202 } 203 204 if (truncate && enoughRows) 205 { 206 if (i != Format.edgeitems) 207 arrStr ~= addedFrame ~ newRow.join(" ") ~ addedFrame ~ Frame.newline; 208 else 209 arrStr ~= addedFrame ~ (cast(string) Frame.truncStr) 210 .repeat(maxRow.strlen).join ~ addedFrame ~ Frame.newline; 211 } 212 else 213 { 214 arrStr ~= addedFrame ~ newRow.join(" ") ~ addedFrame ~ Frame.newline; 215 } 216 } 217 return arrStr; 218 } 219 220 private string prettyFrame(T)(T arrSlice, string addedFrame, Tuple!(ulong, 221 "strlen", string[], "row") maxRow, bool truncate) 222 if (arrSlice.shape.length > 2) 223 { 224 string arrStr; 225 for (ulong i; i < arrSlice.shape[0]; i++) 226 { 227 string padding = getPadding!(typeof(arrSlice[i].shape))(arrSlice[i].shape, maxRow.strlen); 228 arrStr ~= addedFrame ~ Frame.ltAngle ~ padding ~ Frame.rtAngle ~ addedFrame ~ Frame.newline; 229 arrStr ~= prettyFrame!(typeof(arrSlice[i]))(arrSlice[i], 230 addedFrame ~ Frame.vBar, maxRow, truncate); 231 arrStr ~= addedFrame ~ Frame.lbAngle ~ padding ~ Frame.rbAngle ~ addedFrame ~ Frame.newline; 232 } 233 234 return arrStr; 235 } 236 237 // Check if an array can be truncated. 238 private bool canTruncate(T)(T arrSlice) 239 { 240 return (arrSlice.flattened.length > Format.threshold) || ((arrSlice.shape.length == 1) 241 && (arrSlice.getStrLength > Format.lineWidth)) ? true : false; 242 } 243 244 /++ 245 Pretty-print D array. 246 +/ 247 string prettyArr(T)(T arr) 248 in 249 { 250 assert(isConvertibleToSlice!(typeof(arr))); 251 } 252 do 253 { 254 string arrStr; 255 auto arrSlice = arr.fuse; // convert to Mir Slice by GC allocating with fuse 256 // check if we need array truncation 257 const bool truncate = arrSlice.canTruncate; 258 auto maxRow = arrSlice.getMaxStrLenAndMaxRow(truncate); 259 string padding = getPadding!(typeof(arrSlice.shape))(arrSlice.shape, maxRow.strlen); 260 arrStr ~= Frame.ltAngle ~ padding ~ Frame.rtAngle ~ Frame.newline; 261 static if (arrSlice.shape.length > 1) 262 { 263 arrStr ~= prettyFrame!(typeof(arrSlice))(arrSlice, Frame.vBar, maxRow, truncate); 264 } 265 else 266 { 267 arrStr ~= prettyFrame!(typeof(arrSlice))(arrSlice, truncate); 268 } 269 arrStr ~= Frame.lbAngle ~ padding ~ Frame.rbAngle ~ Frame.newline; 270 return arrStr; 271 } 272 273 unittest 274 { 275 import std.range : chunks; 276 277 // TODO: getShape tests 278 279 int[] a0 = [200, 1, -3, 0, 0, 8501, 23]; 280 string testa0 = "┌ ┐ 281 │200 1 -3 0 0 8501 23│ 282 └ ┘ 283 "; 284 assert(prettyArr!(typeof(a0))(a0) == testa0); 285 286 auto a = [5, 2].iota!int(1).fuse; 287 auto maxa = a.getMaxStrLenAndMaxRow(a.canTruncate); 288 assert(getPadding!(typeof(a.shape))(a.shape, maxa.strlen).length == 4); 289 string testa = "┌ ┐ 290 │1 2│ 291 │3 4│ 292 │5 6│ 293 │7 8│ 294 │9 10│ 295 └ ┘ 296 "; 297 assert(prettyArr!(typeof(a))(a) == testa); 298 299 auto b = [2, 2, 6].iota!int(1).fuse; 300 auto maxb = b.getMaxStrLenAndMaxRow(b.canTruncate); 301 assert(getPadding!(typeof(b.shape))(b.shape, maxb.strlen).length == 19); 302 string testb = "┌ ┐ 303 │┌ ┐│ 304 ││ 1 2 3 4 5 6││ 305 ││ 7 8 9 10 11 12││ 306 │└ ┘│ 307 │┌ ┐│ 308 ││13 14 15 16 17 18││ 309 ││19 20 21 22 23 24││ 310 │└ ┘│ 311 └ ┘ 312 "; 313 assert(prettyArr!(typeof(b))(b) == testb); 314 int[] carr = [ 315 1000, 21, 1232, 4, 5, 36, 1207, 18, 9, 10, -1, 12, 133, -14, 21915, 16 316 ]; 317 auto c = carr.chunks(2).array.chunks(4).array.chunks(2).array; // jagged D array 318 string testc = "┌ ┐ 319 │┌ ┐│ 320 ││┌ ┐││ 321 │││ 1000 21│││ 322 │││ 1232 4│││ 323 │││ 5 36│││ 324 │││ 1207 18│││ 325 ││└ ┘││ 326 ││┌ ┐││ 327 │││ 9 10│││ 328 │││ -1 12│││ 329 │││ 133 -14│││ 330 │││21915 16│││ 331 ││└ ┘││ 332 │└ ┘│ 333 └ ┘ 334 "; 335 assert(prettyArr!(typeof(c))(c) == testc); 336 337 auto d = [3, 1, 2, 1].iota!int(1).fuse; 338 string testd = "┌ ┐ 339 │┌ ┐│ 340 ││┌ ┐││ 341 │││1│││ 342 │││2│││ 343 ││└ ┘││ 344 │└ ┘│ 345 │┌ ┐│ 346 ││┌ ┐││ 347 │││3│││ 348 │││4│││ 349 ││└ ┘││ 350 │└ ┘│ 351 │┌ ┐│ 352 ││┌ ┐││ 353 │││5│││ 354 │││6│││ 355 ││└ ┘││ 356 │└ ┘│ 357 └ ┘ 358 "; 359 assert(prettyArr!(typeof(d))(d) == testd); 360 361 auto e = [2, 3, 6, 6].iota!int(1).fuse; 362 string teste = "┌ ┐ 363 │┌ ┐│ 364 ││┌ ┐││ 365 │││ 1 2 3 4 5 6│││ 366 │││ 7 8 9 10 11 12│││ 367 │││ 13 14 15 16 17 18│││ 368 │││ 19 20 21 22 23 24│││ 369 │││ 25 26 27 28 29 30│││ 370 │││ 31 32 33 34 35 36│││ 371 ││└ ┘││ 372 ││┌ ┐││ 373 │││ 37 38 39 40 41 42│││ 374 │││ 43 44 45 46 47 48│││ 375 │││ 49 50 51 52 53 54│││ 376 │││ 55 56 57 58 59 60│││ 377 │││ 61 62 63 64 65 66│││ 378 │││ 67 68 69 70 71 72│││ 379 ││└ ┘││ 380 ││┌ ┐││ 381 │││ 73 74 75 76 77 78│││ 382 │││ 79 80 81 82 83 84│││ 383 │││ 85 86 87 88 89 90│││ 384 │││ 91 92 93 94 95 96│││ 385 │││ 97 98 99 100 101 102│││ 386 │││103 104 105 106 107 108│││ 387 ││└ ┘││ 388 │└ ┘│ 389 │┌ ┐│ 390 ││┌ ┐││ 391 │││109 110 111 112 113 114│││ 392 │││115 116 117 118 119 120│││ 393 │││121 122 123 124 125 126│││ 394 │││127 128 129 130 131 132│││ 395 │││133 134 135 136 137 138│││ 396 │││139 140 141 142 143 144│││ 397 ││└ ┘││ 398 ││┌ ┐││ 399 │││145 146 147 148 149 150│││ 400 │││151 152 153 154 155 156│││ 401 │││157 158 159 160 161 162│││ 402 │││163 164 165 166 167 168│││ 403 │││169 170 171 172 173 174│││ 404 │││175 176 177 178 179 180│││ 405 ││└ ┘││ 406 ││┌ ┐││ 407 │││181 182 183 184 185 186│││ 408 │││187 188 189 190 191 192│││ 409 │││193 194 195 196 197 198│││ 410 │││199 200 201 202 203 204│││ 411 │││205 206 207 208 209 210│││ 412 │││211 212 213 214 215 216│││ 413 ││└ ┘││ 414 │└ ┘│ 415 └ ┘ 416 "; 417 assert(e.prettyArr == teste); 418 419 auto f = [100, 5].iota!int(1).fuse; 420 string testf = "┌ ┐ 421 │ 1 2 3 4 5│ 422 │ 6 7 8 9 10│ 423 │ 11 12 13 14 15│ 424 │░░░░░░░░░░░░░░░░░░░│ 425 │486 487 488 489 490│ 426 │491 492 493 494 495│ 427 │496 497 498 499 500│ 428 └ ┘ 429 "; 430 431 auto g = [100, 100].iota!int(1).fuse; 432 string testg = "┌ ┐ 433 │ 1 2 3 ░ 98 99 100│ 434 │ 101 102 103 ░ 198 199 200│ 435 │ 201 202 203 ░ 298 299 300│ 436 │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ 437 │9701 9702 9703 ░ 9798 9799 9800│ 438 │9801 9802 9803 ░ 9898 9899 9900│ 439 │9901 9902 9903 ░ 9998 9999 10000│ 440 └ ┘ 441 "; 442 443 auto h = [500].iota!int(1).fuse; 444 string testh = "┌ ┐ 445 │1 2 3 ░ 498 499 500│ 446 └ ┘ 447 "; 448 assert(h.prettyArr == testh); 449 450 auto i = [2, 100, 500].iota!int(1).fuse; 451 string testi = "┌ ┐ 452 │┌ ┐│ 453 ││ 1 2 3 ░ 498 499 500││ 454 ││ 501 502 503 ░ 998 999 1000││ 455 ││ 1001 1002 1003 ░ 1498 1499 1500││ 456 ││░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░││ 457 ││48501 48502 48503 ░ 48998 48999 49000││ 458 ││49001 49002 49003 ░ 49498 49499 49500││ 459 ││49501 49502 49503 ░ 49998 49999 50000││ 460 │└ ┘│ 461 │┌ ┐│ 462 ││50001 50002 50003 ░ 50498 50499 50500││ 463 ││50501 50502 50503 ░ 50998 50999 51000││ 464 ││51001 51002 51003 ░ 51498 51499 51500││ 465 ││░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░││ 466 ││98501 98502 98503 ░ 98998 98999 99000││ 467 ││99001 99002 99003 ░ 99498 99499 99500││ 468 ││99501 99502 99503 ░ 99998 99999 100000││ 469 │└ ┘│ 470 └ ┘ 471 "; 472 473 }